// Package storage provides garbage collection functionality for storage backends
package storage

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"
)

// GarbageCollector handles garbage collection of unused data
type GarbageCollector struct {
	basePath       string
	mu             sync.RWMutex
	lastGC         time.Time
	gcPolicy       GCPolicy
	referenceStore *ReferenceStore
	isRunning      bool
}

// GCPolicy defines garbage collection policy
type GCPolicy struct {
	RunInterval      time.Duration // How often to run GC
	MinAge            time.Duration // Minimum age before considering for GC
	BatchSize         int           // Number of items to process in one batch
	DryRun            bool          // If true, only log what would be deleted
	MaxFileSize       int64         // Maximum file size to consider for GC
	ProtectedPatterns []string      // Patterns for protected files
}

// DefaultGCPolicy returns default garbage collection policy
func DefaultGCPolicy() GCPolicy {
	return GCPolicy{
		RunInterval:       24 * time.Hour, // Daily
		MinAge:            7 * 24 * time.Hour, // 7 days
		BatchSize:         1000,
		DryRun:            false,
		MaxFileSize:       100 * 1024 * 1024, // 100MB
		ProtectedPatterns: []string{
			"*.lock",
			"*.tmp",
			"metadata/*",
			"config/*",
			"gc/*",
		},
	}
}

// ReferenceStore tracks references to stored items
type ReferenceStore struct {
	mu          sync.RWMutex
	references  map[string]int64 // hash -> reference count
	lastAccess  map[string]time.Time // hash -> last access time
}

// NewReferenceStore creates a new reference store
func NewReferenceStore() *ReferenceStore {
	return &ReferenceStore{
		references: make(map[string]int64),
		lastAccess: make(map[string]time.Time),
	}
}

// AddReference adds a reference to an item
func (rs *ReferenceStore) AddReference(hash string) {
	rs.mu.Lock()
	defer rs.mu.Unlock()

	rs.references[hash]++
	rs.lastAccess[hash] = time.Now()
}

// RemoveReference removes a reference to an item
func (rs *ReferenceStore) RemoveReference(hash string) {
	rs.mu.Lock()
	defer rs.mu.Unlock()

	if count, exists := rs.references[hash]; exists {
		if count <= 1 {
			delete(rs.references, hash)
			delete(rs.lastAccess, hash)
		} else {
			rs.references[hash]--
		}
	}
}

// GetReferenceCount returns the reference count for an item
func (rs *ReferenceStore) GetReferenceCount(hash string) int64 {
	rs.mu.RLock()
	defer rs.mu.RUnlock()

	return rs.references[hash]
}

// GetLastAccess returns the last access time for an item
func (rs *ReferenceStore) GetLastAccess(hash string) (time.Time, bool) {
	rs.mu.RLock()
	defer rs.mu.RUnlock()

	lastAccess, exists := rs.lastAccess[hash]
	return lastAccess, exists
}

// GetUnreferencedItems returns items with zero references
func (rs *ReferenceStore) GetUnreferencedItems() []string {
	rs.mu.RLock()
	defer rs.mu.RUnlock()

	var unreferenced []string
	for hash, count := range rs.references {
		if count == 0 {
			unreferenced = append(unreferenced, hash)
		}
	}
	return unreferenced
}

// NewGarbageCollector creates a new garbage collector
func NewGarbageCollector(basePath string) (*GarbageCollector, error) {
	gc := &GarbageCollector{
		basePath:       basePath,
		gcPolicy:       DefaultGCPolicy(),
		referenceStore: NewReferenceStore(),
		lastGC:         time.Now(),
	}

	return gc, nil
}

// Start starts the garbage collector
func (gc *GarbageCollector) Start(ctx context.Context) error {
	gc.mu.Lock()
	if gc.isRunning {
		gc.mu.Unlock()
		return fmt.Errorf("garbage collector is already running")
	}
	gc.isRunning = true
	gc.mu.Unlock()

	go gc.run(ctx)
	return nil
}

// Stop stops the garbage collector
func (gc *GarbageCollector) Stop() {
	gc.mu.Lock()
	defer gc.mu.Unlock()
	gc.isRunning = false
}

// run runs the garbage collection loop
func (gc *GarbageCollector) run(ctx context.Context) {
	ticker := time.NewTicker(gc.gcPolicy.RunInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if err := gc.RunGC(ctx); err != nil {
				// Log error but continue running
				fmt.Printf("Garbage collection error: %v\n", err)
			}
		}
	}
}

// RunGC runs garbage collection once
func (gc *GarbageCollector) RunGC(ctx context.Context) error {
	gc.mu.Lock()
	if gc.isRunning {
		gc.mu.Unlock()
		return fmt.Errorf("garbage collection is already running")
	}
	gc.isRunning = true
	gc.mu.Unlock()

	defer func() {
		gc.mu.Lock()
		gc.isRunning = false
		gc.lastGC = time.Now()
		gc.mu.Unlock()
	}()

	fmt.Printf("Starting garbage collection...\n")

	// Phase 1: Scan for unreferenced files
	unreferencedFiles, err := gc.findUnreferencedFiles(ctx)
	if err != nil {
		return fmt.Errorf("failed to find unreferenced files: %w", err)
	}

	// Phase 2: Filter files by age and size
	candidates := gc.filterCandidates(unreferencedFiles)

	// Phase 3: Delete candidates
	deletedCount, freedSpace, err := gc.deleteCandidates(ctx, candidates)
	if err != nil {
		return fmt.Errorf("failed to delete candidates: %w", err)
	}

	fmt.Printf("Garbage collection completed: deleted %d files, freed %d bytes\n",
		deletedCount, freedSpace)

	return nil
}

// findUnreferencedFiles finds files that have no references
func (gc *GarbageCollector) findUnreferencedFiles(ctx context.Context) ([]FileInfo, error) {
	var unreferencedFiles []FileInfo

	err := filepath.Walk(gc.basePath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil // Skip errors
		}

		// Skip directories
		if info.IsDir() {
			return nil
		}

		// Skip protected files
		if gc.isProtectedFile(path) {
			return nil
		}

		// Extract hash from filename (assuming hash is the filename without extension)
		hash := extractHashFromPath(path)
		if hash == "" {
			return nil
		}

		// Check if file has references
		if gc.referenceStore.GetReferenceCount(hash) == 0 {
			unreferencedFiles = append(unreferencedFiles, FileInfo{
				Path:     path,
				Size:     info.Size(),
				ModTime:  info.ModTime(),
				Hash:     hash,
			})
		}

		return nil
	})

	if err != nil {
		return nil, fmt.Errorf("failed to walk directory: %w", err)
	}

	return unreferencedFiles, nil
}

// filterCandidates filters files based on GC policy
func (gc *GarbageCollector) filterCandidates(files []FileInfo) []FileInfo {
	var candidates []FileInfo
	cutoffTime := time.Now().Add(-gc.gcPolicy.MinAge)

	for _, file := range files {
		// Check age
		if file.ModTime.After(cutoffTime) {
			continue
		}

		// Check size
		if file.Size > gc.gcPolicy.MaxFileSize {
			continue
		}

		candidates = append(candidates, file)
	}

	return candidates
}

// deleteCandidates deletes the candidate files
func (gc *GarbageCollector) deleteCandidates(ctx context.Context, candidates []FileInfo) (int, int64, error) {
	var deletedCount int
	var freedSpace int64

	for i, candidate := range candidates {
		if ctx.Err() != nil {
			return deletedCount, freedSpace, ctx.Err()
		}

		// Process in batches
		if i > 0 && i%gc.gcPolicy.BatchSize == 0 {
			// Small delay between batches to avoid overwhelming the system
			time.Sleep(100 * time.Millisecond)
		}

		if gc.gcPolicy.DryRun {
			fmt.Printf("[DRY RUN] Would delete: %s (size: %d bytes)\n",
				candidate.Path, candidate.Size)
			deletedCount++
			freedSpace += candidate.Size
			continue
		}

		// Delete the file
		if err := os.Remove(candidate.Path); err != nil {
			fmt.Printf("Failed to delete %s: %v\n", candidate.Path, err)
			continue
		}

		// Remove from reference store
		gc.referenceStore.RemoveReference(candidate.Hash)

		deletedCount++
		freedSpace += candidate.Size
	}

	return deletedCount, freedSpace, nil
}

// isProtectedFile checks if a file is protected from GC
func (gc *GarbageCollector) isProtectedFile(path string) bool {
	relPath, err := filepath.Rel(gc.basePath, path)
	if err != nil {
		return true // Protect files we can't resolve
	}

	for _, pattern := range gc.gcPolicy.ProtectedPatterns {
		matched, err := filepath.Match(pattern, relPath)
		if err == nil && matched {
			return true
		}
	}

	return false
}

// extractHashFromPath extracts hash from file path
func extractHashFromPath(path string) string {
	// Simple implementation: use filename without extension as hash
	filename := filepath.Base(path)
	ext := filepath.Ext(filename)
	if ext != "" {
		return filename[:len(filename)-len(ext)]
	}
	return filename
}

// AddReference adds a reference to prevent GC
func (gc *GarbageCollector) AddReference(hash string) {
	gc.referenceStore.AddReference(hash)
}

// RemoveReference removes a reference
func (gc *GarbageCollector) RemoveReference(hash string) {
	gc.referenceStore.RemoveReference(hash)
}

// GetReferenceCount returns the reference count for an item
func (gc *GarbageCollector) GetReferenceCount(hash string) int64 {
	return gc.referenceStore.GetReferenceCount(hash)
}

// GetGCStats returns garbage collection statistics
func (gc *GarbageCollector) GetGCStats() GCStats {
	gc.mu.RLock()
	defer gc.mu.RUnlock()

	return GCStats{
		LastGC:           gc.lastGC,
		IsRunning:        gc.isRunning,
		ReferenceCount:   int64(len(gc.referenceStore.references)),
	}
}

// SetGCPolicy updates the GC policy
func (gc *GarbageCollector) SetGCPolicy(policy GCPolicy) {
	gc.mu.Lock()
	defer gc.mu.Unlock()

	gc.gcPolicy = policy
}

// FileInfo represents information about a file for GC
type FileInfo struct {
	Path    string
	Size    int64
	ModTime time.Time
	Hash    string
}

// GCStats represents garbage collection statistics
type GCStats struct {
	LastGC         time.Time
	IsRunning      bool
	ReferenceCount int64
}